package wallet

import (
	"fmt"
	"os"
	"path/filepath"
	"sync"

	"github.com/sirupsen/logrus"

	"github.com/SkycoinProject/skycoin/src/cipher"
	"github.com/SkycoinProject/skycoin/src/cipher/bip44"
	"github.com/SkycoinProject/skycoin/src/util/file"
)

// TransactionsFinder interface for finding address related transaction hashes
type TransactionsFinder interface {
	AddressesActivity(addrs []cipher.Address) ([]bool, error)
}

// Service wallet service struct
type Service struct {
	sync.RWMutex
	wallets Wallets
	config  Config
	// fingerprints is used to check for duplicate deterministic wallets
	fingerprints map[string]string
}

// Config wallet service config
type Config struct {
	WalletDir       string
	CryptoType      CryptoType
	EnableWalletAPI bool
	EnableSeedAPI   bool
	Bip44Coin       *bip44.CoinType
}

// NewConfig creates a default Config
func NewConfig() Config {
	bc := bip44.CoinTypeSkycoin
	return Config{
		WalletDir:       "./",
		CryptoType:      DefaultCryptoType,
		EnableWalletAPI: false,
		EnableSeedAPI:   false,
		Bip44Coin:       &bc,
	}
}

// NewService new wallet service
func NewService(c Config) (*Service, error) {
	serv := &Service{
		config:       c,
		fingerprints: make(map[string]string),
	}

	if !serv.config.EnableWalletAPI {
		return serv, nil
	}

	if err := os.MkdirAll(c.WalletDir, os.FileMode(0700)); err != nil {
		return nil, fmt.Errorf("failed to create wallet directory %s: %v", c.WalletDir, err)
	}

	// Removes .wlt.bak files before loading wallets
	if err := removeBackupFiles(serv.config.WalletDir); err != nil {
		return nil, fmt.Errorf("remove .wlt.bak files in %v failed: %v", serv.config.WalletDir, err)
	}

	// Load all wallets from disk
	w, err := loadWallets(serv.config.WalletDir)
	if err != nil {
		return nil, fmt.Errorf("failed to load all wallets: %v", err)
	}

	// Abort if there are duplicate wallets (identified by fingerprint) on disk
	if wltID, fp, hasDup := w.containsDuplicate(); hasDup {
		return nil, fmt.Errorf("duplicate wallet found with fingerprint %s in file %q", fp, wltID)
	}

	// Abort if there are empty deterministic wallets on disk
	if wltID, hasEmpty := w.containsEmpty(); hasEmpty {
		return nil, fmt.Errorf("empty wallet file found: %q", wltID)
	}

	serv.setWallets(w)

	fields := logrus.Fields{
		"walletDir": serv.config.WalletDir,
	}
	if serv.config.Bip44Coin != nil {
		fields["bip44Coin"] = *serv.config.Bip44Coin
	}
	logger.WithFields(fields).Debug("wallet.NewService complete")

	return serv, nil
}

// WalletDir returns the configured wallet directory
func (serv *Service) WalletDir() (string, error) {
	serv.Lock()
	defer serv.Unlock()
	if !serv.config.EnableWalletAPI {
		return "", ErrWalletAPIDisabled
	}
	return serv.config.WalletDir, nil
}

func (serv *Service) updateOptions(opts Options) Options {
	// Apply service-configured default settings for wallet options
	if opts.Encrypt && opts.CryptoType == "" {
		opts.CryptoType = serv.config.CryptoType
	}
	if opts.Type == WalletTypeBip44 && opts.Bip44Coin == nil && serv.config.Bip44Coin != nil {
		c := *serv.config.Bip44Coin
		opts.Bip44Coin = &c
	}
	return opts
}

// CreateWallet creates a wallet with the given wallet file name and options.
// A address will be automatically generated by default.
func (serv *Service) CreateWallet(wltName string, options Options, tf TransactionsFinder) (Wallet, error) {
	serv.Lock()
	defer serv.Unlock()
	if !serv.config.EnableWalletAPI {
		return nil, ErrWalletAPIDisabled
	}
	if wltName == "" {
		wltName = serv.generateUniqueWalletFilename()
	}

	options = serv.updateOptions(options)
	return serv.loadWallet(wltName, options, tf)
}

// loadWallet loads wallet from seed and scan the first N addresses
func (serv *Service) loadWallet(wltName string, options Options, tf TransactionsFinder) (Wallet, error) {
	options = serv.updateOptions(options)
	w, err := NewWalletScanAhead(wltName, options, tf)
	if err != nil {
		return nil, err
	}

	fingerprint := w.Fingerprint()
	if fingerprint != "" {
		if _, ok := serv.fingerprints[fingerprint]; ok {
			// Note: collection wallets do not have fingerprints
			switch w.Type() {
			case WalletTypeDeterministic, WalletTypeBip44:
				return nil, ErrSeedUsed
			case WalletTypeXPub:
				return nil, ErrXPubKeyUsed
			default:
				logger.WithFields(logrus.Fields{
					"walletType":  w.Type(),
					"fingerprint": fingerprint,
				}).Panic("Unhandled wallet type after fingerprint conflict")
			}
		}
	}

	if err := serv.wallets.add(w); err != nil {
		return nil, err
	}

	if err := Save(w, serv.config.WalletDir); err != nil {
		// If save fails, remove the added wallet
		serv.wallets.remove(w.Filename())
		return nil, err
	}

	if fingerprint != "" {
		serv.fingerprints[fingerprint] = w.Filename()
	}

	return w.Clone(), nil
}

func (serv *Service) generateUniqueWalletFilename() string {
	wltName := NewWalletFilename()
	for {
		if w := serv.wallets.get(wltName); w == nil {
			break
		}
		wltName = NewWalletFilename()
	}

	return wltName
}

// EncryptWallet encrypts wallet with password
func (serv *Service) EncryptWallet(wltID string, password []byte) (Wallet, error) {
	serv.Lock()
	defer serv.Unlock()
	if !serv.config.EnableWalletAPI {
		return nil, ErrWalletAPIDisabled
	}

	w, err := serv.getWallet(wltID)
	if err != nil {
		return nil, err
	}

	if w.IsEncrypted() {
		return nil, ErrWalletEncrypted
	}

	if err := Lock(w, password, serv.config.CryptoType); err != nil {
		return nil, err
	}

	// Save to disk first
	if err := Save(w, serv.config.WalletDir); err != nil {
		return nil, err
	}

	// Sets the encrypted wallet
	serv.wallets.set(w)
	return w, nil
}

// DecryptWallet decrypts wallet with password
func (serv *Service) DecryptWallet(wltID string, password []byte) (Wallet, error) {
	serv.Lock()
	defer serv.Unlock()
	if !serv.config.EnableWalletAPI {
		return nil, ErrWalletAPIDisabled
	}

	w, err := serv.getWallet(wltID)
	if err != nil {
		return nil, err
	}

	// Returns error if wallet is not encrypted
	if !w.IsEncrypted() {
		return nil, ErrWalletNotEncrypted
	}

	// Unlocks the wallet
	unlockWlt, err := Unlock(w, password)
	if err != nil {
		return nil, err
	}

	// Updates the wallet file
	if err := Save(unlockWlt, serv.config.WalletDir); err != nil {
		return nil, err
	}

	// Sets the decrypted wallet in memory
	serv.wallets.set(unlockWlt)
	return unlockWlt, nil
}

// NewAddresses generate address entries in given wallet,
// return nil if wallet does not exist.
// Set password as nil if the wallet is not encrypted, otherwise the password must be provided.
func (serv *Service) NewAddresses(wltID string, password []byte, num uint64) ([]cipher.Address, error) {
	serv.Lock()
	defer serv.Unlock()

	if !serv.config.EnableWalletAPI {
		return nil, ErrWalletAPIDisabled
	}

	w, err := serv.getWallet(wltID)
	if err != nil {
		return nil, err
	}

	var addrs []cipher.Address
	f := func(wlt Wallet) error {
		var err error
		addrs, err = wlt.GenerateSkycoinAddresses(num)
		return err
	}

	if w.IsEncrypted() {
		if err := GuardUpdate(w, password, f); err != nil {
			return nil, err
		}
	} else {
		if len(password) != 0 {
			return nil, ErrWalletNotEncrypted
		}

		if err := f(w); err != nil {
			return nil, err
		}
	}

	// Checks if the wallet file is writable
	wf := filepath.Join(serv.config.WalletDir, w.Filename())
	if !file.IsWritable(wf) {
		return nil, ErrWalletPermission
	}

	// Save the wallet first
	if err := Save(w, serv.config.WalletDir); err != nil {
		return nil, err
	}

	serv.wallets.set(w)

	return addrs, nil
}

// GetSkycoinAddresses returns all addresses in given wallet
func (serv *Service) GetSkycoinAddresses(wltID string) ([]cipher.Address, error) {
	serv.RLock()
	defer serv.RUnlock()
	if !serv.config.EnableWalletAPI {
		return nil, ErrWalletAPIDisabled
	}

	w, err := serv.getWallet(wltID)
	if err != nil {
		return nil, err
	}

	return w.GetSkycoinAddresses()
}

// GetWallet returns wallet by id
func (serv *Service) GetWallet(wltID string) (Wallet, error) {
	serv.RLock()
	defer serv.RUnlock()
	if !serv.config.EnableWalletAPI {
		return nil, ErrWalletAPIDisabled
	}

	return serv.getWallet(wltID)
}

// returns the clone of the wallet of given id
func (serv *Service) getWallet(wltID string) (Wallet, error) {
	w := serv.wallets.get(wltID)
	if w == nil {
		return nil, ErrWalletNotExist
	}
	return w.Clone(), nil
}

// GetWallets returns all wallet clones
func (serv *Service) GetWallets() (Wallets, error) {
	serv.RLock()
	defer serv.RUnlock()
	if !serv.config.EnableWalletAPI {
		return nil, ErrWalletAPIDisabled
	}

	wlts := make(Wallets, len(serv.wallets))
	for k, w := range serv.wallets {
		wlts[k] = w.Clone()
	}
	return wlts, nil
}

// UpdateWalletLabel updates the wallet label
func (serv *Service) UpdateWalletLabel(wltID, label string) error {
	serv.Lock()
	defer serv.Unlock()
	if !serv.config.EnableWalletAPI {
		return ErrWalletAPIDisabled
	}

	w, err := serv.getWallet(wltID)
	if err != nil {
		return err
	}

	w.SetLabel(label)

	if err := Save(w, serv.config.WalletDir); err != nil {
		return err
	}

	serv.wallets.set(w)
	return nil
}

// UnloadWallet removes wallet of given wallet id from the service
func (serv *Service) UnloadWallet(wltID string) error {
	serv.Lock()
	defer serv.Unlock()
	if !serv.config.EnableWalletAPI {
		return ErrWalletAPIDisabled
	}

	wlt := serv.wallets.get(wltID)
	if wlt != nil {
		if fp := wlt.Fingerprint(); fp != "" {
			delete(serv.fingerprints, fp)
		}
	}

	serv.wallets.remove(wltID)
	return nil
}

func (serv *Service) setWallets(wlts Wallets) {
	serv.wallets = wlts

	for wltID, wlt := range wlts {
		if fp := wlt.Fingerprint(); fp != "" {
			serv.fingerprints[fp] = wltID
		}
	}
}

// GetWalletSeed returns seed and seed passphrase of encrypted wallet of given wallet id
// Returns ErrWalletNotEncrypted if it's not encrypted
func (serv *Service) GetWalletSeed(wltID string, password []byte) (string, string, error) {
	serv.RLock()
	defer serv.RUnlock()
	if !serv.config.EnableWalletAPI {
		return "", "", ErrWalletAPIDisabled
	}

	if !serv.config.EnableSeedAPI {
		return "", "", ErrSeedAPIDisabled
	}

	w, err := serv.getWallet(wltID)
	if err != nil {
		return "", "", err
	}

	if !w.IsEncrypted() {
		return "", "", ErrWalletNotEncrypted
	}

	var seed, seedPassphrase string
	if err := GuardView(w, password, func(wlt Wallet) error {
		seed = wlt.Seed()
		seedPassphrase = wlt.SeedPassphrase()
		return nil
	}); err != nil {
		return "", "", err
	}

	return seed, seedPassphrase, nil
}

// UpdateSecrets opens a wallet for modification of secret data and saves it safely
func (serv *Service) UpdateSecrets(wltID string, password []byte, f func(Wallet) error) error {
	serv.Lock()
	defer serv.Unlock()
	if !serv.config.EnableWalletAPI {
		return ErrWalletAPIDisabled
	}

	w, err := serv.getWallet(wltID)
	if err != nil {
		return err
	}

	if w.IsEncrypted() {
		if err := GuardUpdate(w, password, f); err != nil {
			return err
		}
	} else if len(password) != 0 {
		return ErrWalletNotEncrypted
	} else {
		if err := f(w); err != nil {
			return err
		}
	}

	// Save the wallet first
	if err := Save(w, serv.config.WalletDir); err != nil {
		return err
	}

	serv.wallets.set(w)

	return nil
}

// Update opens a wallet for modification of non-secret data and saves it safely
func (serv *Service) Update(wltID string, f func(Wallet) error) error {
	serv.Lock()
	defer serv.Unlock()
	if !serv.config.EnableWalletAPI {
		return ErrWalletAPIDisabled
	}

	w, err := serv.getWallet(wltID)
	if err != nil {
		return err
	}

	if err := f(w); err != nil {
		return err
	}

	// Save the wallet first
	if err := Save(w, serv.config.WalletDir); err != nil {
		return err
	}

	serv.wallets.set(w)

	return nil
}

// ViewSecrets opens a wallet for reading secret data
func (serv *Service) ViewSecrets(wltID string, password []byte, f func(Wallet) error) error {
	serv.RLock()
	defer serv.RUnlock()
	if !serv.config.EnableWalletAPI {
		return ErrWalletAPIDisabled
	}

	w, err := serv.getWallet(wltID)
	if err != nil {
		return err
	}

	if w.IsEncrypted() {
		return GuardView(w, password, f)
	} else if len(password) != 0 {
		return ErrWalletNotEncrypted
	} else {
		return f(w)
	}
}

// View opens a wallet for reading non-secret data
func (serv *Service) View(wltID string, f func(Wallet) error) error {
	serv.RLock()
	defer serv.RUnlock()
	if !serv.config.EnableWalletAPI {
		return ErrWalletAPIDisabled
	}

	w, err := serv.getWallet(wltID)
	if err != nil {
		return err
	}

	return f(w)
}

// RecoverWallet recovers an encrypted wallet from seed.
// The recovered wallet will be encrypted with the new password, if provided.
func (serv *Service) RecoverWallet(wltName, seed, seedPassphrase string, password []byte) (Wallet, error) {
	serv.Lock()
	defer serv.Unlock()
	if !serv.config.EnableWalletAPI {
		return nil, ErrWalletAPIDisabled
	}

	w, err := serv.getWallet(wltName)
	if err != nil {
		return nil, err
	}

	if !w.IsEncrypted() {
		return nil, ErrWalletNotEncrypted
	}

	switch w.Type() {
	case WalletTypeDeterministic, WalletTypeBip44:
	default:
		return nil, ErrWalletTypeNotRecoverable
	}

	// Create a wallet from this seed and compare the fingerprint
	w2, err := NewWallet(wltName, Options{
		Type:           w.Type(),
		Coin:           w.Coin(),
		Seed:           seed,
		SeedPassphrase: seedPassphrase,
		GenerateN:      1,
	})
	if err != nil {
		err = NewError(fmt.Errorf("RecoverWallet failed to create temporary wallet for fingerprint comparison: %v", err))
		logger.Critical().WithError(err).Error()
		return nil, err
	}
	if w.Fingerprint() != w2.Fingerprint() {
		return nil, ErrWalletRecoverSeedWrong
	}

	// Create a new wallet with the same number of addresses, encrypting if needed
	w3, err := NewWallet(wltName, Options{
		Type:           w.Type(),
		Coin:           w.Coin(),
		Label:          w.Label(),
		Seed:           seed,
		SeedPassphrase: seedPassphrase,
		Encrypt:        len(password) != 0,
		Password:       password,
		CryptoType:     w.CryptoType(),
		GenerateN:      uint64(w.EntriesLen()),
	})
	if err != nil {
		return nil, err
	}

	// Preserve the timestamp of the old wallet
	w3.SetTimestamp(w.Timestamp())

	// Save to disk
	if err := Save(w3, serv.config.WalletDir); err != nil {
		return nil, err
	}

	serv.wallets.set(w3)

	return w3.Clone(), nil
}
